package com.powernode.ssm.serviceImpl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.powernode.ssm.config.LoginFreqControlProperties;
import com.powernode.ssm.config.RabbitMQConfig;
import com.powernode.ssm.dao.RoleInfoDao;
import com.powernode.ssm.dao.UserDao;
import com.powernode.ssm.pojo.bo.EmaiNotificationMessage;
import com.powernode.ssm.pojo.bo.SmsMessage;
import com.powernode.ssm.pojo.po.H5UserinfoPo;
import com.powernode.ssm.pojo.po.RoleInfoPo;
import com.powernode.ssm.pojo.po.UserLoginAttempt;
import com.powernode.ssm.pojo.vo.LoginVo;
import com.powernode.ssm.pojo.vo.TokenInfo;
import com.powernode.ssm.service.*;
import com.powernode.ssm.utils.*;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

import static com.powernode.ssm.config.RabbitMQStatusMonitor.isMqAvailable;

@Slf4j
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private SpringUtils springUtils;

    @Autowired
    UserDao userDao;

    @Autowired
    RoleInfoDao roleInfoDao;

    // 注入的是代理类 RedisServiceWithFallback，支持降级
    @Autowired
    private RedisServiceWithFallback redisService;

    @Value("${myapp.code}")
    private String appCode;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private UserLoginTokenService userLoginTokenService;

    @Autowired
    private UserLoginAttemptService userLoginAttemptService; // 降级频控服务（已注入）

    // ✅ 注入统一配置类（关键依赖）
    @Autowired
    private LoginFreqControlProperties loginFreqControlProperties;

    /**
     * 用户第一次登录，随机生成token，存入redis后并返回前端
     * 用户在过期时间内登录，自动覆盖旧的过期时间，redis保留旧的token，也返回旧token到前端
     *
     * @param request
     * @param item
     * @return
     */
    @Override
    public ResponseUtils login(HttpServletRequest request, LoginVo item) {

        log.info("登录前置处理，校验IP是否频繁访问...");
        int loginAllowedFlag = isLoginAllowed(request); // ✅ 调用整合后的频控方法（含降级）
        if (loginAllowedFlag == -1) {
            return new ResponseUtils<>(ReturnCodeEnum.LOGIN_BLOCK_ERR.getCode(), ReturnCodeEnum.LOGIN_BLOCK_ERR.getMsg(), null);
        } else if (loginAllowedFlag == -2) {
            return new ResponseUtils<>(ReturnCodeEnum.LOGIN_BUSY_ERR.getCode(), ReturnCodeEnum.LOGIN_BUSY_ERR.getMsg(), null);
        }

        String userid = item.getUserid();
        log.info("校验用户名和密码...");
        if (userDao.checkUserId(item) == 0) {
            return new ResponseUtils<>(ReturnCodeEnum.USER_NOT_FOUND.getCode(), ReturnCodeEnum.USER_NOT_FOUND.getMsg(), null);
        } else if (userDao.checkUserIdAndPassWord(item) == 0) {
            return new ResponseUtils<>(ReturnCodeEnum.PASSWORD_ERR.getCode(), ReturnCodeEnum.PASSWORD_ERR.getMsg(), null);
        }

        log.info("加密成token...");
        // ✅ 替换：从配置类获取 token 默认过期时间（原写死 300 秒）
        long baseExpireTime = loginFreqControlProperties.getTokenExpireSeconds();
        String token = getToken(userid, baseExpireTime);

        log.info("组装包含登录人信息的JSON...");
        H5UserinfoPo h5UserinfoPo = userDao.queryUserById(userid, appCode);
        JSONObject jsonObject = getUserJson(h5UserinfoPo, token);

        springUtils.saveLogWhenLogin("登录", h5UserinfoPo, request);

        sendMsg(h5UserinfoPo);

        if (StrUtil.isNotBlank(h5UserinfoPo.getParam3())){
            sendEmail(h5UserinfoPo);
        }else {
            log.info("用户 {} 登录成功，但邮箱为空，不进行发送邮件", h5UserinfoPo.getUserName());
        }


        return new ResponseUtils<>(ReturnCodeEnum.SUCCESS.getCode(), ReturnCodeEnum.SUCCESS.getMsg(), jsonObject);
    }

    private void sendEmail(H5UserinfoPo h5UserinfoPo) {
        if (!isMqAvailable()) {
            log.info("🔒 RabbitMQ 当前不可用（可能服务未启动或已断开），跳过发送登录邮件通知");
            return;
        }
        log.info("用户 {} 登录成功，已发送登录通知到MQ，邮箱：{}", h5UserinfoPo.getUserName(), h5UserinfoPo.getParam3());

        // 4. 构造登录通知消息
        EmaiNotificationMessage message = new EmaiNotificationMessage();
        message.setUsername(h5UserinfoPo.getUserName());
        message.setEmailAddress(h5UserinfoPo.getParam3());//收件人地址
        message.setLoginTime(LocalDateTime.now());

        // 5. 发送到 RabbitMQ 队列，由消费者异步处理邮件发送
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EMAIL_NOTIFICATION_QUEUE,
                message
        );
    }

    private void sendMsg(H5UserinfoPo h5UserinfoPo) {
        // 如果 MQ 不可用，直接屏蔽发送逻辑
        if (!isMqAvailable()) {
            log.info("🔒 RabbitMQ 当前不可用（可能服务未启动或已断开），跳过发送登录短信通知");
            return;
        }
        log.info("登录成功，准备发送短信通知...");

        String mobileNo = h5UserinfoPo.getMobileNo();
        String message = "您于 " + DateUtil.now() + " 登录了系统，请注意账号安全。";
        SmsMessage smsMessage = new SmsMessage(mobileNo, message);
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.SMS_QUEUE,
                smsMessage
        );
    }

    private JSONObject getUserJson(H5UserinfoPo h5UserinfoPo, String token) {
        RoleInfoPo roleInfoPo = roleInfoDao.selectOneRoleInfo(appCode, h5UserinfoPo.getRoleId());
        String menuInfo = "";
        if (roleInfoPo != null) {
            menuInfo = roleInfoPo.getRolemenu();
        }
        JSONObject jsonObject = new JSONObject();
        jsonObject.putOpt("deptNo", h5UserinfoPo.getDeptNo());
        jsonObject.putOpt("userOrg", h5UserinfoPo.getUserOrg());
        jsonObject.putOpt("userId", h5UserinfoPo.getUserId());
        jsonObject.putOpt("userName", h5UserinfoPo.getUserName());
        jsonObject.putOpt("roleId", h5UserinfoPo.getRoleId());
        jsonObject.putOpt("userLevel", h5UserinfoPo.getUserLevel());
        jsonObject.putOpt("mobileNo", h5UserinfoPo.getMobileNo());
        jsonObject.putOpt("menuIfo", menuInfo);

        JSONObject subTokenObject = new JSONObject();
        String traceId = MDC.get("traceId");
        subTokenObject.putOpt("token", token);
        subTokenObject.putOpt("traceId", traceId);
        jsonObject.putOpt("tokenInfo", subTokenObject);
        return jsonObject;
    }

    /**
     * 生成或获取用户登录 Token
     * <p>
     * 逻辑说明：
     * - 如果 Redis 可用：优先使用 Redis 存储/获取 token（你原有逻辑，可能覆盖或刷新）
     * - 如果 Redis 不可用：
     *   - 先查询数据库中该用户是否已有未过期的 token（通过 user_login_token 表）
     *   - 如果存在未过期 token → 直接返回旧 token（优化体验，避免频繁变更）
     *   - 如果不存在或已过期 → 生成新 token，存入数据库，返回新 token
     * </p>
     *
     * @param userid      用户ID
     * @param baseExpireTime token 基础过期时间（单位：秒）
     * @return 生成的或已有的有效 token
     */
    private String getToken(String userid, long baseExpireTime) {
        // 每次加密都会生成不同的 token（保持你的安全设计）
        String token = TokenUtils.encrypt(userid);
        String key = appCode + "_login_" + userid;

        // ======================
        // 1. Redis 可用时的逻辑（保持你原有逻辑）
        // ======================
        if (redisService.isRedisAvailable()) {
            Boolean set = redisService.setObjectWithExpire(
                    key, token, baseExpireTime + RandomUtil.randomLong(0, 1000), TimeUnit.SECONDS
            );
            if (set) {
                log.info("用户第一次登录，存入 Redis，key = {}", key);
            } else {
                log.info("用户重复登录，不存入 Redis，刷新过期时间，key = {}", key);
                token = redisService.getObject(key, String.class);
                redisService.resetObjectWithExpire(
                        key, token, baseExpireTime + RandomUtil.randomLong(0, 1000), TimeUnit.SECONDS
                );
            }
            return token;
        }
        // ======================
        // 2. Redis 不可用时：降级逻辑（优化版：优先返回旧 token）
        // ======================
        else {
            log.warn("[降级] Redis 不可用，启用降级逻辑，查询数据库是否存在有效 token，用户 = {}", userid);

            // ① 先尝试从数据库查询是否已有未过期的 token
            TokenInfo oldTokenInfo = userLoginTokenService.getTokenIfExists(userid);

            if (oldTokenInfo != null) {
                String oldToken = oldTokenInfo.getToken();
                LocalDateTime expireTime = oldTokenInfo.getExpireTime();

                log.info("[降级] 用户 {} 存在未过期的旧 token，直接返回旧 token，过期时间 = {}", userid, expireTime);

                // 🔒 补充：更新该 token 的过期时间 = 当前时间 + baseExpireTime + 随机时间
                LocalDateTime newExpireTime = LocalDateTime.now().plusSeconds(baseExpireTime + RandomUtil.randomLong(0, 1000));

                log.info("[降级] 用户 {} 的旧 token 过期时间将刷新为 {}", userid, newExpireTime);
                userLoginTokenService.updateTokenExpireTime(userid, newExpireTime);

                return oldToken;
            }

            // ② 没有有效旧 token，生成新 token
            log.info("[降级] 用户 {} 无有效旧 token，生成新 token", userid);
            token = TokenUtils.encrypt(userid);

            // ③ 存储新 token 到数据库（降级存储）
            saveTokenToDb(userid, token, baseExpireTime + RandomUtil.randomLong(0, 1000));

            return token;
        }
    }

    /**
     * ✅ 整合后的登录频控方法（含 Redis 降级逻辑）
     * 原逻辑：Redis 频控（短期/长期次数 + 封禁）
     * 降级逻辑：Redis 不可用时，查询 user_login_attempt 表做频控
     */
    public int isLoginAllowed(HttpServletRequest request) {
        String ip = IpUtils.getClientIp(request); // ✅ 获取客户端真实 IP

        // ===========================
        // 1. 优先检查 Redis 是否可用（原逻辑）
        // ===========================
        if (redisService.isRedisAvailable()) {
            log.info("[频控] Redis 可用，使用 Redis 做 IP 频控，IP = {}", ip);
            return originalRedisBasedIsLoginAllowed(request, ip); // 调用原 Redis 频控逻辑
        }

        // ===========================
        // 2. Redis 不可用时，降级到 user_login_attempt 表做频控（新增核心逻辑）
        // ===========================
        else {
            log.warn("[频控] Redis 不可用，启用 user_login_attempt 表降级频控，IP = {}", ip);
            return degradedDbBasedIsLoginAllowed(ip); // 调用降级后的数据库频控逻辑
        }
    }

    /**
     * 原 Redis 频控逻辑（提取为独立方法，便于复用）
     */
    private int originalRedisBasedIsLoginAllowed(HttpServletRequest request, String ip) {
        // ✅ 替换：从配置类获取频控参数（原写死 5, 60, 20, 600, 3600）
        int shortAttempts = loginFreqControlProperties.getFreqControl().getShortConfig().getMaxAttempts();       // 短时间内最大登录次数
        int shortTimeout = loginFreqControlProperties.getFreqControl().getShortConfig().getWindowSeconds();     // 短单位时间（秒）
        int longAttempts = loginFreqControlProperties.getFreqControl().getLongConfig().getMaxAttempts();        // 长时间内最大登录次数
        int longTimeout = loginFreqControlProperties.getFreqControl().getLongConfig().getWindowMinutes() * 60;  // 长单位时间（秒）
        int blockTimeout = loginFreqControlProperties.getFreqControl().getBlock().getDurationMinutes() * 60;    // 封禁时间（秒，1小时）

        // === 第一步：检查是否已被封禁 ===
        String blockKey = appCode + "_login_attempt_block_" + ip;
        Boolean isBlocked = redisService.hasKey(blockKey);
        if (Boolean.TRUE.equals(isBlocked)) {
            log.warn("[频控] IP = {} 已被封禁，拒绝登录", ip);
            return -1; // 封禁错误码
        }

        // === 第二步：短期请求频率控制（如60秒内最多5次）→ 拒绝登录但不封禁 ===
        String shortAttemptKey = appCode + "_login_attempt_short_" + ip;
        Long shortCount = redisService.increment(shortAttemptKey);
        if (shortCount != null && shortCount == 1) {
            redisService.expire(shortAttemptKey, shortTimeout, TimeUnit.SECONDS); // 首次设置过期时间
        }
        if (shortCount != null && shortCount > shortAttempts) {
            log.warn("[频控] IP = {} 短期请求频繁（{} > {}），拒绝登录", ip, shortCount, shortAttempts);
            return -2; // 频繁错误码
        }

        // === 第三步：长期请求频率控制（如10分钟内最多20次）→ 超过则封禁IP ===
        String longAttemptKey = appCode + "_login_attempt_long_" + ip;
        Long longCount = redisService.increment(longAttemptKey);
        if (longCount != null && longCount == 1) {
            redisService.expire(longAttemptKey, longTimeout, TimeUnit.SECONDS); // 首次设置过期时间
        }
        if (longCount != null && longCount > longAttempts) {
            log.warn("[频控] IP = {} 长期请求频繁（{} > {}），触发封禁", ip, longCount, longAttempts);
            redisService.increment(blockKey); // 增加封禁计数
            redisService.expire(blockKey, blockTimeout, TimeUnit.SECONDS); // 封禁1小时
            redisService.delete(shortAttemptKey); // 清除短期计数
            redisService.delete(longAttemptKey); // 清除长期计数
            return -1; // 封禁错误码
        }

        // === 通过风控，允许继续登录流程 ===
        return 0;
    }

    /**
     * 降级后的数据库频控逻辑（Redis 不可用时调用）
     * 1. 获取客户端 IP（IpUtils.getClientIp(request)）
     * 2. 查询 user_login_attempt 表，获取该 IP 的登录尝试记录
     * 3. 检查是否已被封禁：
     *    - 是 → 拒绝登录（返回 -1）
     *    - 否 → 检查封禁是否过期（自动解封）
     * 4. 检查短期尝试次数（60秒内最多5次）：
     *    - 超限 → 触发封禁（返回 -1）
     *    - 未超限 → 检查短期窗口是否过期（过期则重置计数）
     * 5. 检查长期尝试次数（10分钟内最多20次）：
     *    - 超限 → 触发封禁（返回 -1）
     *    - 未超限 → 检查长期窗口是否过期（过期则重置计数）
     * 6. 记录本次登录尝试（更新短期/长期次数或过期时间）
     * 7. 允许登录（返回 0）
     */
    private int degradedDbBasedIsLoginAllowed(String ip) {
        // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
        // 1. 查询 IP 当前登录尝试状态
        // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
        UserLoginAttempt attempt = userLoginAttemptService.findByIp(ip);
        LocalDateTime now = LocalDateTime.now();

        // ✅ 替换：从配置类获取频控参数（与 Redis 逻辑一致）
        int shortAttempts = loginFreqControlProperties.getFreqControl().getShortConfig().getMaxAttempts();       // 短时间内最大登录次数
        int shortTimeout = loginFreqControlProperties.getFreqControl().getShortConfig().getWindowSeconds();     // 短单位时间（秒）
        int longAttempts = loginFreqControlProperties.getFreqControl().getLongConfig().getMaxAttempts();        // 长时间内最大登录次数
        int longTimeout = loginFreqControlProperties.getFreqControl().getLongConfig().getWindowMinutes() * 60;  // 长单位时间（秒）
        int blockTimeout = loginFreqControlProperties.getFreqControl().getBlock().getDurationMinutes() * 60;    // 封禁时间（秒，1小时）

        // === 1.1 检查是否已被封禁 ===
        if (attempt != null && attempt.getIsBlocked() != null && attempt.getIsBlocked() == 1) {
            if (attempt.getBlockExpireTime() != null && attempt.getBlockExpireTime().isAfter(now)) {
                log.warn("[降级频控] IP = {} 当前被封禁，直到 {}", ip, attempt.getBlockExpireTime());
                return -1; // 封禁错误码
            } else {
                // 封禁已过期，重置状态（可选：自动解封）
                log.info("[降级频控] IP = {} 封禁已过期，自动解封", ip);
                userLoginAttemptService.resetBlockStatus(ip, now); // 需在 Service 中实现
            }
        }

        // === 1.2 检查短期尝试是否超限（如60秒内最多5次） ===
        if (attempt != null && attempt.getShortAttempts() != null && attempt.getShortAttempts() >= shortAttempts) {  // ✅ 使用配置参数
            if (attempt.getShortExpireTime() != null && attempt.getShortExpireTime().isAfter(now)) {
                log.warn("[降级频控] IP = {} 短期尝试超限（{} >= {}），触发封禁", ip, attempt.getShortAttempts(), shortAttempts);
                userLoginAttemptService.blockIp(attempt, ip, now); // 需在 Service 中实现
                return -1; // 封禁错误码
            } else {
                // 短期窗口已过，重置计数
                log.info("[降级频控] IP = {} 短期窗口已过，重置计数", ip);
                userLoginAttemptService.resetShortWindow(ip, now); // 需在 Service 中实现
            }
        }

        // === 1.3 检查长期尝试是否超限（如10分钟内最多20次） ===
        if (attempt != null && attempt.getLongAttempts() != null && attempt.getLongAttempts() >= longAttempts) {  // ✅ 使用配置参数
            if (attempt.getLongExpireTime() != null && attempt.getLongExpireTime().isAfter(now)) {
                log.warn("[降级频控] IP = {} 长期尝试超限（{} >= {}），触发封禁", ip, attempt.getLongAttempts(), longAttempts);
                userLoginAttemptService.blockIp(attempt, ip, now); // 需在 Service 中实现
                return -1; // 封禁错误码
            } else {
                // 长期窗口已过，重置计数
                log.info("[降级频控] IP = {} 长期窗口已过，重置计数", ip);
                userLoginAttemptService.resetLongWindow(ip, now); // 需在 Service 中实现
            }
        }

        // === 2. 记录本次登录尝试（用于统计次数） ===
        userLoginAttemptService.recordLoginAttempt(ip);
        // ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

        // === 通过风控，允许继续登录流程 ===
        return 0;
    }

    /**
     * ✅ 新增方法：降级时将 token 存入数据库（user_login_token 表）
     * 用于在 Redis 不可用时，依然能记录用户登录 token，便于后续管理或查询
     */
    private void saveTokenToDb(String userId, String token, long expireSeconds) {
        try {
            LocalDateTime expireTime = LocalDateTime.now().plusSeconds(expireSeconds);
            userLoginTokenService.saveOrUpdateToken(userId, token, expireTime);
            log.info("[降级存储] 用户 {} 的 token 已降级存储到数据库，过期时间: {}", userId, expireTime);
        } catch (Exception e) {
            log.error("[降级存储] 用户 {} 的 token 存入数据库失败，token={}", userId, token, e);
        }
    }
}